/// <reference types="Cypress" />

context('Cypress Wait Until', () => {
  beforeEach(() => {
    cy.visit('http://localhost:3000/')
  })

  it('Should work with an immediately-satisfied condition', () => {
    const COOKIE_NAME = 'immediate-cookie'
    const EXPECTED_COOKIE_VALUE = 'Set'
    cy.get('#' + COOKIE_NAME).click()

    const checkFunction = () =>
      cy
        .getCookie(COOKIE_NAME)
        .then((cookieValue) => cookieValue && cookieValue.value === EXPECTED_COOKIE_VALUE)

    cy.waitUntil(checkFunction)

    cy.getCookie(COOKIE_NAME).then((cookieValue) =>
      expect(cookieValue.value).to.be.equal(EXPECTED_COOKIE_VALUE)
    )
  })

  it('Should work with a condition satisfied after a random delay', () => {
    const COOKIE_NAME = 'after-a-while-cookie'
    const EXPECTED_COOKIE_VALUE = 'Set'
    cy.get('#' + COOKIE_NAME).click()

    const checkFunction = () =>
      cy
        .getCookie(COOKIE_NAME)
        .then((cookieValue) => cookieValue && cookieValue.value === EXPECTED_COOKIE_VALUE)

    cy.waitUntil(checkFunction).then(() => {
      cy.getCookie(COOKIE_NAME).then((cookieValue) =>
        expect(cookieValue.value).to.be.equal(EXPECTED_COOKIE_VALUE)
      )
    })
  })

  it('Should run once if waitUntil timeout is less than checkFunction execution time', () => {
    let checks = 0
    const checkFunction = () => {
      cy.wait(500).then(() => {
        checks++
        return false
      })
    }

    cy.once('fail', () => {
      expect(checks).to.equal(1)
    })

    cy.waitUntil(checkFunction, { timeout: 400 })
  })

  it('Should run 3 times if checkFunction + interval is 2s and timeout is 5s', () => {
    let checks = 0
    const checkFunction = () => {
      cy.wait(1500).then(() => {
        checks++
        return false
      })
    }

    cy.once('fail', () => {
      expect(checks).to.equal(3)
    })

    cy.waitUntil(checkFunction, { timeout: 5000, interval: 500 })
  })

  it('Should apply options correctly', () => {
    const COOKIE_NAME = 'after-a-while-cookie'
    const EXPECTED_COOKIE_VALUE = 'Set'

    cy.once('fail', (err) => {
      expect(err.message).to.be.equal('Timed out retrying')
    })

    cy.get('#' + COOKIE_NAME).click()

    const checkFunction = () =>
      cy
        .getCookie(COOKIE_NAME)
        .then((cookieValue) => cookieValue && cookieValue.value === EXPECTED_COOKIE_VALUE)

    cy.waitUntil(checkFunction, {
      interval: 100,
      timeout: 900,
    })
  })

  it('Should log a custom logging description', () => {
    const COOKIE_NAME = 'after-a-while-cookie'
    const EXPECTED_COOKIE_VALUE = 'Set'

    cy.once('fail', (err) => {
      expect(err.message).to.be.equal('Timed out retrying')
    })

    cy.get('#' + COOKIE_NAME).click()

    const checkFunction = () =>
      cy
        .getCookie(COOKIE_NAME)
        .then((cookieValue) => cookieValue && cookieValue.value === EXPECTED_COOKIE_VALUE)

    cy.waitUntil(checkFunction, {
      interval: 100,
      timeout: 900,
    })
  })

  it('Should check value equality check', () => {
    const COOKIE_NAME = 'change-after-a-while-cookie'
    const EXPECTED_COOKIE_VALUE = '7'
    cy.get('#' + COOKIE_NAME).click()

    const checkFunction = () =>
      cy
        .getCookie(COOKIE_NAME)
        .then((cookieValue) => cookieValue && cookieValue.value === EXPECTED_COOKIE_VALUE)

    cy.waitUntil(checkFunction)

    cy.getCookie(COOKIE_NAME).then((cookieValue) =>
      expect(cookieValue.value).to.be.equal(EXPECTED_COOKIE_VALUE)
    )
  })

  it('Should make the test fail with an unsatisfied condition', () => {
    const COOKIE_NAME = 'unknwon-cookie'
    const EXPECTED_COOKIE_VALUE = 'Set'

    cy.once('fail', (err) => {
      expect(err.message).to.be.equal('Timed out retrying')
    })

    const checkFunction = () =>
      cy
        .getCookie(COOKIE_NAME)
        .then((cookieValue) => cookieValue && cookieValue.value === EXPECTED_COOKIE_VALUE)

    cy.waitUntil(checkFunction)
  })

  it('Should work sync', () => {
    const checkFunction = () => true

    cy.waitUntil(checkFunction)
  })

  it('Should work sync with retries', () => {
    let n = 4
    const checkFunction = () => {
      n--
      return n < 0
    }

    cy.waitUntil(checkFunction)
  })

  it('`checkFunction` should be a function', () => {
    const ERROR_MESSAGE = '`checkFunction` parameter should be a function. Found: true'

    cy.once('fail', (err) => expect(err.message).to.be.equal(ERROR_MESSAGE))
    cy.waitUntil(true)
  })

  it('Should accept a custom error message', () => {
    const COOKIE_NAME = 'unknwon-cookie'
    const EXPECTED_COOKIE_VALUE = 'Set'

    cy.once('fail', (err) => {
      expect(err.message).to.be.equal('Custom error message')
    })

    const checkFunction = () =>
      cy
        .getCookie(COOKIE_NAME)
        .then((cookieValue) => cookieValue && cookieValue.value === EXPECTED_COOKIE_VALUE)

    cy.waitUntil(checkFunction, { errorMsg: 'Custom error message' })
  })

  it('Should accept a custom error message that is a function', () => {
    const COOKIE_NAME = 'unknwon-cookie'
    const EXPECTED_COOKIE_VALUE = 'Set'
    let dynamicValue = 'before'

    cy.once('fail', (err) => {
      expect(err.message).to.be.equal('Custom error message - after')
    })

    const checkFunction = () =>
      cy.getCookie(COOKIE_NAME).then((cookieValue) => {
        dynamicValue = 'after'
        return cookieValue && cookieValue.value === EXPECTED_COOKIE_VALUE
      })

    cy.waitUntil(checkFunction, {
      errorMsg: () => `Custom error message - ${dynamicValue}`,
    })
  })

  it('Should pass the options passed to the plugin to the custom error message function', () => {
    const options = {
      timeout: 100,
      interval: 50,
    }
    const errorMsg = (result, pluginOptions) => {
      expect(result).to.eq(0)
      expect(pluginOptions).to.deep.eq({
        ...options,
        description: 'waitUntil',
        log: true,
        customMessage: [options],
        logger: Cypress.log,
        verbose: false,
        customCheckMessage: undefined,
      })
      return 'Params expectations ok'
    }
    options.errorMsg = errorMsg

    let lastResult = 1
    const checkFunction = () => {
      lastResult = 0
      return lastResult
    }

    cy.once('fail', (err) => {
      expect(err.message).to.be.equal('Params expectations ok')
    })
    cy.waitUntil(checkFunction, options)
  })

  it('Should pass the result to the next command', () => {
    const result = 10

    const checkFunction = () => result
    const asyncCheckFunction = () => Promise.resolve(result)
    const chainableCheckFunction = () => cy.wrap(result).then((wrappedResult) => wrappedResult)

    cy.waitUntil(checkFunction).should('eq', result)
    cy.waitUntil(asyncCheckFunction).should('eq', result)
    cy.waitUntil(chainableCheckFunction).should('eq', result)
  })

  it('Should wait between every check', () => {
    const interval = 100
    let previousTimestamp

    const checkFunction = () => {
      const previousTimestampBackup = previousTimestamp
      const newTimestamp = Date.now()
      previousTimestamp = newTimestamp
      if (previousTimestampBackup) {
        const diff = newTimestamp - previousTimestampBackup
        return diff >= interval
      }
      return false
    }

    const asyncCheckFunction = () => Promise.resolve(checkFunction())
    const chainableCheckFunction = () => cy.wrap().then(() => checkFunction())

    cy.log('Sync function')
    cy.waitUntil(checkFunction, { interval })
    cy.log('Async function')
    cy.waitUntil(asyncCheckFunction, { interval })
    cy.log('Chainable function')
    cy.waitUntil(chainableCheckFunction, { interval })
  })

  it('Should be chainable', () => {
    const result = 10
    const checkFunction = () => result
    const checkFunctionWithSubject = (subject) => subject

    cy.waitUntil(checkFunction).should('eq', result)

    cy.wrap(result).waitUntil(checkFunctionWithSubject).should('eq', result)
  })

  it('Should leverage Cypress.log', () => {
    const checkFunction = () => true

    const logger = {
      log: (...params) => Cypress.log(...params),
    }
    const spy = cy.spy(logger, 'log')

    cy.waitUntil(checkFunction, { logger: logger.log }).then(() => {
      expect(spy).to.have.been.called
      const lastCallArgs = spy.lastCall.args[0]
      expect(lastCallArgs).deep.include({ name: 'waitUntil' })
    })
  })

  it('Should accept a custom log description', () => {
    const checkFunction = () => true
    const description = 'custom description'

    const logger = {
      log: (...params) => Cypress.log(...params),
    }
    const spy = cy.spy(logger, 'log')

    cy.waitUntil(checkFunction, { logger: logger.log, description }).then(() => {
      expect(spy).to.have.been.called
      const lastCallArgs = spy.lastCall.args[0]
      expect(lastCallArgs).deep.include({ name: description })
    })
  })

  it('Should accept a custom message', () => {
    const checkFunction = () => true
    const customMessage = 'custom message'

    const logger = {
      log: (...params) => Cypress.log(...params),
    }
    const spy = cy.spy(logger, 'log')
    const options = { logger: logger.log, customMessage }

    cy.waitUntil(checkFunction, options).then(() => {
      expect(spy).to.have.been.called
      const lastCallArgs = spy.lastCall.args[0]
      expect(lastCallArgs.message).deep.include(customMessage)
    })
  })

  it('Should allow to turn off logging', () => {
    const checkFunction = () => true

    const logger = {
      log: (...params) => Cypress.log(...params),
    }
    const spy = cy.spy(logger, 'log')

    cy.waitUntil(checkFunction, { logger: logger.log, log: false }).then(() => {
      expect(spy).not.to.have.been.called
    })
  })

  it('Should log verbosely every single check', () => {
    let checks = 0
    const checkFunction = () => {
      checks++
      return checks > 1
    }

    const logger = {
      log: (...params) => Cypress.log(...params),
    }
    const spy = cy.spy(logger, 'log')
    const options = { logger: logger.log, verbose: true }

    cy.waitUntil(checkFunction, options).then(() => {
      const calls = spy.getCalls()
      const expected = [
        {
          name: 'waitUntil',
        },
        {
          name: 'waitUntil',
          message: 'false',
        },
        {
          name: 'waitUntil',
          message: 'true',
        },
      ]
      expect(calls).to.have.lengthOf(expected.length)
      for (let n = calls.length, i = 0; i < n; i++) {
        expect(calls[i].args[0]).deep.include(expected[i])
      }
    })
  })

  it('Should accept a `customCheckMessage` option', () => {
    const checkFunction = () => true

    const logger = {
      log: (...params) => Cypress.log(...params),
    }
    const spy = cy.spy(logger, 'log')
    const customCheckMessage = 'custom message check'
    const options = { logger: logger.log, verbose: true, customCheckMessage }

    cy.waitUntil(checkFunction, options).then(() => {
      expect(spy.getCalls()[1].args[0].message.toString()).to.include(customCheckMessage)
    })
  })

  // test useful just to printout all the available options, screenshot them, and add them to the README
  it('Options explanation', () => {
    let checks = 0
    const checkFunction = () => {
      checks++
      return checks > 2
    }
    const checkFunction2 = () => {
      checks++
      return checks > 5
    }

    const options = {
      // log options
      description: 'description',
      customMessage: 'customMessage',
      verbose: true,
      customCheckMessage: 'customCheckMessage',
    }

    cy.waitUntil(checkFunction, { verbose: true })
    cy.waitUntil(checkFunction2, options)
  })
})
